import * as util from './util';
import DataSet from './DataSet';

/**
 * DataView
 *
 * a dataview offers a filtered view on a dataset or an other dataview.
 *
 * @param {DataSet | DataView} data
 * @param {Object} [options]   Available options: see method get
 *
 * @constructor DataView
 */
class DataView {
  constructor(data, options) {
    this._data = null;
    this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
    this.length = 0; // number of items in the DataView
    this._options = options || {};
    this._fieldId = 'id'; // name of the field containing id
    this._subscribers = {}; // event subscribers

    const me = this;
    this.listener = function () {
      me._onEvent(...arguments);
    };

    this.setData(data);
  }

  // TODO: implement a function .config() to dynamically update things like configured filter
  // and trigger changes accordingly

  /**
   * Set a data source for the view
   * @param {DataSet | DataView} data
   */
  setData(data) {
    let ids;
    let id;
    let i;
    let len;
    let items;

    if (this._data) {
      // unsubscribe from current dataset
      if (this._data.off) {
        this._data.off('*', this.listener);
      }

      // trigger a remove of all items in memory
      ids = this._data.getIds({filter: this._options && this._options.filter});
      items = [];

      for (i = 0, len = ids.length; i < len; i++) {
        items.push(this._data._data[ids[i]]);
      }

      this._ids = {};
      this.length = 0;
      this._trigger('remove', {items: ids, oldData: items});
    }

    this._data = data;

    if (this._data) {
      // update fieldId
      this._fieldId = this._options.fieldId ||
          (this._data && this._data.options && this._data.options.fieldId) ||
          'id';

      // trigger an add of all added items
      ids = this._data.getIds({filter: this._options && this._options.filter});
      for (i = 0, len = ids.length; i < len; i++) {
        id = ids[i];
        this._ids[id] = true;
      }
      this.length = ids.length;
      this._trigger('add', {items: ids});

      // subscribe to new dataset
      if (this._data.on) {
        this._data.on('*', this.listener);
      }
    }
  }

  /**
   * Refresh the DataView. Useful when the DataView has a filter function
   * containing a variable parameter.
   */
  refresh() {
    let id;
    let i;
    let len;
    const ids = this._data.getIds({filter: this._options && this._options.filter});
    const oldIds = Object.keys(this._ids);
    const newIds = {};
    const addedIds = [];
    const removedIds = [];
    const removedItems = [];

    // check for additions
    for (i = 0, len = ids.length; i < len; i++) {
      id = ids[i];
      newIds[id] = true;
      if (!this._ids[id]) {
        addedIds.push(id);
        this._ids[id] = true;
      }
    }

    // check for removals
    for (i = 0, len = oldIds.length; i < len; i++) {
      id = oldIds[i];
      if (!newIds[id]) {
        removedIds.push(id);
        removedItems.push(this._data._data[id]);
        delete this._ids[id];
      }
    }

    this.length += addedIds.length - removedIds.length;

    // trigger events
    if (addedIds.length) {
      this._trigger('add', {items: addedIds});
    }
    if (removedIds.length) {
      this._trigger('remove', {items: removedIds, oldData: removedItems});
    }
  }

  /**
   * Get data from the data view
   *
   * Usage:
   *
   *     get()
   *     get(options: Object)
   *     get(options: Object, data: Array | DataTable)
   *
   *     get(id: Number)
   *     get(id: Number, options: Object)
   *     get(id: Number, options: Object, data: Array | DataTable)
   *
   *     get(ids: Number[])
   *     get(ids: Number[], options: Object)
   *     get(ids: Number[], options: Object, data: Array | DataTable)
   *
   * Where:
   *
   * {number | string} id         The id of an item
   * {number[] | string{}} ids    An array with ids of items
   * {Object} options             An Object with options. Available options:
   *                              {string} [type] Type of data to be returned. Can
   *                                              be 'DataTable' or 'Array' (default)
   *                              {Object.<string, string>} [convert]
   *                              {string[]} [fields] field names to be returned
   *                              {function} [filter] filter items
   *                              {string | function} [order] Order the items by
   *                                  a field name or custom sort function.
   * {Array | DataTable} [data]   If provided, items will be appended to this
   *                              array or table. Required in case of Google
   *                              DataTable.
   * @param {Array} args
   * @return {DataSet|DataView}
   */
  get() {
    // eslint-disable-line no-unused-vars
    const me = this;

    // parse the arguments
    let ids;

    let options;
    let data;
    const firstType = util.getType(arguments[0]);
    if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
      // get(id(s) [, options] [, data])
      ids = arguments[0];  // can be a single id or an array with ids
      options = arguments[1];
      data = arguments[2];
    }
    else {
      // get([, options] [, data])
      options = arguments[0];
      data = arguments[1];
    }

    // extend the options with the default options and provided options
    const viewOptions = util.extend({}, this._options, options);

    // create a combined filter method when needed
    if (this._options.filter && options && options.filter) {
      viewOptions.filter = item => me._options.filter(item) && options.filter(item)
    }

    // build up the call to the linked data set
    const getArguments = [];
    if (ids != undefined) {
      getArguments.push(ids);
    }
    getArguments.push(viewOptions);
    getArguments.push(data);

    return this._data && this._data.get(...getArguments);
  }

  /**
   * Get ids of all items or from a filtered set of items.
   * @param {Object} [options]    An Object with options. Available options:
   *                              {function} [filter] filter items
   *                              {string | function} [order] Order the items by
   *                                  a field name or custom sort function.
   * @return {Array.<string|number>} ids
   */
  getIds(options) {
    let ids;

    if (this._data) {
      const defaultFilter = this._options.filter;
      let filter;

      if (options && options.filter) {
        if (defaultFilter) {
          filter = item => defaultFilter(item) && options.filter(item)
        }
        else {
          filter = options.filter;
        }
      }
      else {
        filter = defaultFilter;
      }

      ids = this._data.getIds({
        filter,
        order: options && options.order
      });
    }
    else {
      ids = [];
    }

    return ids;
  }

  /**
   * Map every item in the dataset.
   * @param {function} callback
   * @param {Object} [options]    Available options:
   *                              {Object.<string, string>} [type]
   *                              {string[]} [fields] filter fields
   *                              {function} [filter] filter items
   *                              {string | function} [order] Order the items by
   *                                  a field name or custom sort function.
   * @return {Object[]} mappedItems
   */
  map(callback, options) {
    let mappedItems = [];
    if (this._data) {
      const defaultFilter = this._options.filter;
      let filter;

      if (options && options.filter) {
        if (defaultFilter) {
          filter = item => defaultFilter(item) && options.filter(item)
        }
        else {
          filter = options.filter;
        }
      }
      else {
        filter = defaultFilter;
      }

      mappedItems = this._data.map(callback,{
        filter,
        order: options && options.order
      });
    }
    else {
      mappedItems = [];
    }

    return mappedItems;
  }

  /**
   * Get the DataSet to which this DataView is connected. In case there is a chain
   * of multiple DataViews, the root DataSet of this chain is returned.
   * @return {DataSet} dataSet
   */
  getDataSet() {
    let dataSet = this;
    while (dataSet instanceof DataView) {
      dataSet = dataSet._data;
    }
    return dataSet || null;
  }

  /**
   * Event listener. Will propagate all events from the connected data set to
   * the subscribers of the DataView, but will filter the items and only trigger
   * when there are changes in the filtered data set.
   * @param {string} event
   * @param {Object | null} params
   * @param {string} senderId
   * @private
   */
  _onEvent(event, params, senderId) {
    let i;
    let len;
    let id;
    let item;
    const ids = params && params.items;
    const addedIds = [];
    const updatedIds = [];
    const removedIds = [];
    const oldItems = [];
    const updatedItems = [];
    const removedItems = [];

    if (ids && this._data) {
      switch (event) {
        case 'add':
          // filter the ids of the added items
          for (i = 0, len = ids.length; i < len; i++) {
            id = ids[i];
            item = this.get(id);
            if (item) {
              this._ids[id] = true;
              addedIds.push(id);
            }
          }

          break;

        case 'update':
          // determine the event from the views viewpoint: an updated
          // item can be added, updated, or removed from this view.
          for (i = 0, len = ids.length; i < len; i++) {
            id = ids[i];
            item = this.get(id);

            if (item) {
              if (this._ids[id]) {
                updatedIds.push(id);
                updatedItems.push(params.data[i]);
                oldItems.push(params.oldData[i]);
              }
              else {
                this._ids[id] = true;
                addedIds.push(id);
              }
            }
            else {
              if (this._ids[id]) {
                delete this._ids[id];
                removedIds.push(id);
                removedItems.push(params.oldData[i]);
              }
              else {
                // nothing interesting for me :-(
              }
            }
          }

          break;

        case 'remove':
          // filter the ids of the removed items
          for (i = 0, len = ids.length; i < len; i++) {
            id = ids[i];
            if (this._ids[id]) {
              delete this._ids[id];
              removedIds.push(id);
              removedItems.push(params.oldData[i]);
            }
          }

          break;
      }

      this.length += addedIds.length - removedIds.length;

      if (addedIds.length) {
        this._trigger('add', {items: addedIds}, senderId);
      }
      if (updatedIds.length) {
        this._trigger('update', {items: updatedIds, oldData: oldItems, data: updatedItems}, senderId);
      }
      if (removedIds.length) {
        this._trigger('remove', {items: removedIds, oldData: removedItems}, senderId);
      }
    }
  }
}

// copy subscription functionality from DataSet
DataView.prototype.on = DataSet.prototype.on;
DataView.prototype.off = DataSet.prototype.off;
DataView.prototype._trigger = DataSet.prototype._trigger;

export default DataView;
